import { getLanguage } from '@knno/i18n';
import { DICT } from '../dict/dict';
import { readFilesAsDataURL, selectFiles } from '../tools/upload';
import { hex2blob, isDescendant } from '../tools/utils';
import { ButtonPrimary } from './button/button';
import { Dialog } from './dialog/dialog';
import { Input } from './input/input';

export const ALLOW_CONTROLS = new Set([
	'Backspace',
	'Delete',
	'Tab',
	'ArrowLeft',
	'ArrowRight',
	'ArrowUp',
	'ArrowDown',
	'Home',
	'End',
	'PageUp',
	'PageDown',
]);

document.addEventListener('dragover', (evt) => {
	evt.preventDefault();
});
document.addEventListener('drop', (evt) => {
	evt.preventDefault();
});
document.addEventListener('mouseup', (evt) => {
	if (evt.button === 1) {
		evt.preventDefault();
	}
});

export function selectAll(elem: HTMLElement) {
	elem.focus();
	const range = document.createRange();
	range.selectNodeContents(elem);
	if (!elem.textContent) {
		range.collapse(true);
	}
	const sel = window.getSelection();
	if (sel) {
		sel.removeAllRanges();
		sel.addRange(range);
	}
}

export function findAllTextNodesWithNodeIterator(root: Node): Text[] {
	const textNodes: Text[] = [];
	// 创建 NodeIterator 实例，仅遍历文本节点
	const nodeIterator = document.createNodeIterator(
		root,
		NodeFilter.SHOW_TEXT, // 只显示文本节点
		null,
	);

	let currentNode = nodeIterator.nextNode() as Text | null;
	while (currentNode) {
		textNodes.push(currentNode);
		currentNode = nodeIterator.nextNode() as Text | null;
	}
	return textNodes;
}

export function selectTextEnd(elem: HTMLElement) {
	elem.focus();
	const range = document.createRange();
	// const nodeIterator = document.createNodeIterator(
	// 	elem,
	// 	NodeFilter.SHOW_TEXT, // 只显示文本节点
	// 	null
	// );
	// let begin = true;
	// let currentNode = nodeIterator.nextNode() as Text | null;
	// while (currentNode) {
	// 	if (begin) {
	// 		range.setStart(currentNode, currentNode.length);
	// 		range.setEnd(currentNode, currentNode.length);
	// 		begin = false;
	// 	} else {
	// 		range.setEnd(currentNode, currentNode.length);
	// 	}
	// 	currentNode = nodeIterator.nextNode() as Text | null;
	// }
	range.selectNodeContents(elem);
	range.collapse();
	const sel = window.getSelection();
	if (sel) {
		sel.removeAllRanges();
		sel.addRange(range);
	}
}

export function isEditable(elem: HTMLElement): boolean {
	let el: HTMLElement | null = elem;
	while (el) {
		if (el.contentEditable === 'false') return false;
		else if (el.contentEditable === 'true' || el.contentEditable === 'plaintext-only') {
			return true;
		} else {
			el = el.parentElement;
		}
	}
	return false;
}

export function findCursorElement(useActiveElement = true): Element | null {
	const sel = window.getSelection();
	if (!sel || sel.rangeCount === 0) return null;
	const range = sel.getRangeAt(0);
	if (useActiveElement && document.activeElement) {
		if (!isDescendant([document.activeElement], range.startContainer)) {
			return document.activeElement as HTMLElement;
		}
	}
	if (range.startContainer.nodeName === '#text') {
		return range.startContainer.parentElement;
	} else {
		return range.startContainer as HTMLElement;
	}
}

export function findParent<T extends HTMLElement>(node: Node, parentNodeName: string): T | null {
	let search: HTMLElement | null = node.parentElement;
	while (search) {
		if (search.nodeName.toLowerCase() === parentNodeName.toLowerCase()) return search as T;
		search = search.parentElement;
	}
	return null;
}

export function findEventElement<T extends HTMLElement>(
	event: Event,
	parentNodeName: string,
): T | null {
	let search: Element | null = event.target as Element;
	while (search) {
		if (search.nodeName.toLowerCase() === parentNodeName.toLowerCase()) return search as T;
		search = search.parentElement;
	}
	return null;
}

export function getIndexOfParent(element: Element, nodeName?: string): number {
	const parent = element.parentElement;
	if (!parent) return -1;
	return Array.from(parent.children)
		.filter((tr) => (nodeName ? tr.nodeName === nodeName : true))
		.indexOf(element);
}

export function getNodeIndexOfParent(node: ChildNode, nodeName?: string): number {
	const parent = node.parentElement;
	if (!parent) return -1;
	return Array.from(parent.childNodes)
		.filter((tr) => (nodeName ? tr.nodeName === nodeName : true))
		.indexOf(node);
}

export function getTableActiveCellPos(
	table: HTMLTableElement,
): { row: number; column: number } | null {
	let elem = findCursorElement();
	if (!elem) elem = document.activeElement;
	if (!elem) return null;
	if (!isDescendant([table], elem)) {
		return null;
	}
	while (elem && elem !== table && elem.nodeName !== 'TD') {
		elem = elem?.parentElement ?? null;
	}
	if (elem && elem.nodeName === 'TD') {
		const tr = elem.parentElement;
		if (!tr) return null;
		const colIndex = getIndexOfParent(elem, 'TD');
		if (colIndex < 0) return null;
		const parent = tr.parentElement;
		if (!parent) return null;
		const rowIndex = getIndexOfParent(tr, 'TR');
		if (rowIndex >= 0) {
			return { row: rowIndex, column: colIndex };
		} else {
			return null;
		}
	} else {
		return null;
	}
}

export function activeTableCell(
	table: HTMLTableElement,
	row: number,
	column: number,
	toStart = false,
): void {
	const cell = table.querySelector<HTMLTableCellElement>(
		`tr:nth-child(${row + 1})>td:nth-child(${column + 1})`,
	);
	if (cell) {
		if (!isEditable(cell)) {
			(cell as HTMLElement).focus();
		} else if (cell.childNodes.length > 0) {
			const range = document.createRange();
			const textNodes = Array.from(cell.childNodes).filter((n) => n.nodeName === '#text');
			if (textNodes.length > 0) {
				if (toStart) {
					range.selectNodeContents(textNodes.at(0) as ChildNode);
				} else {
					range.selectNodeContents(textNodes.at(-1) as ChildNode);
				}
			} else {
				if (toStart) {
					range.setStart(cell, 0);
				} else {
					range.setStart(cell, cell.childNodes.length - 1);
				}
			}
			range.collapse(toStart);
			setSelection(range);
		} else {
			(cell as HTMLElement).focus();
		}
	}
}

export function isCursorAtBegin(element: HTMLElement): boolean {
	const sel = window.getSelection();
	if (!sel || sel.rangeCount === 0) return false;
	const range = sel.getRangeAt(0);
	// console.log(range);
	if (!range.collapsed) return false;
	if (range.startContainer === element.firstChild) {
		return range.startOffset === 0;
	} else if (range.endContainer === element) {
		return range.startOffset <= element.childNodes.length;
	} else return false;
}

export function isCursorAtEnd(element: HTMLElement): boolean {
	const sel = window.getSelection();
	if (!sel || sel.rangeCount === 0) return false;
	const range = sel.getRangeAt(0);
	// console.log(range);
	if (!range.collapsed) return false;
	if (range.endContainer === element.lastChild) {
		if (range.endContainer.nodeName === '#text') {
			return range.endOffset > (range.endContainer.textContent?.length ?? 0) - 1;
		} else {
			return range.endOffset >= range.endContainer.childNodes.length - 1;
		}
	} else if (range.endContainer === element) {
		return range.endOffset >= element.childNodes.length - 1;
	} else {
		return false;
	}
}

export function getCaretPos(element: HTMLElement): number {
	const sel = window.getSelection();
	if (!sel || sel.rangeCount === 0) return -1;
	const range = sel.getRangeAt(0);
	let pos = 0;
	for (const node of Array.from(element.childNodes)) {
		if (range.startContainer === node) {
			pos += range.startOffset;
			return pos;
		} else {
			pos += node.textContent?.length ?? 0;
		}
	}
	return -1;
}

export function setRangeByPos(x: number, y: number): void {
	const pos = document.caretPositionFromPoint(x, y);
	if (pos) {
		const selection = getSelection();
		selection?.removeAllRanges();
		const range = document.createRange();
		range.selectNode(pos.offsetNode);
		range.setStart(pos.offsetNode, pos.offset);
		range.setEnd(pos.offsetNode, pos.offset);
		range.collapse(true);
		selection?.addRange(range);
	}
}

export function isInFirstLine(element: HTMLElement): boolean {
	const pos = getCaretPos(element);
	if (pos < 0) return false;
	const text = element.textContent ?? '';
	return !text.substring(0, pos).includes('\n');
}

export function isInLastLine(element: HTMLElement): boolean {
	const pos = getCaretPos(element);
	if (pos < 0) return false;
	const text = element.textContent ?? '';
	return !text.substring(pos).includes('\n');
}

export function activeListItem(ul: HTMLUListElement, path: number[]) {
	if (path.length === 0) return;
	const item = ul.children.item(path[0]);
	if (item) {
		if (path.length > 1) {
			const subUL = Array.from(item.children).find((c) => c.nodeName === 'UL');
			if (subUL) {
				activeListItem(subUL as HTMLUListElement, path.slice(1));
			}
		} else {
			let txtNode = Array.from(item.childNodes).findLast((n) => n.nodeName === '#text');
			if (!txtNode) {
				txtNode = item.insertBefore(document.createTextNode(''), item.firstChild);
			}
			const range = document.createRange();
			range.selectNodeContents(txtNode);
			range.collapse(false);
			setSelection(range);
		}
	}
}

export function setSelection(range: Range): void {
	const selection = window.getSelection();
	if (!selection) return;
	selection.removeAllRanges();
	selection.addRange(range);
}

export function isImageElement(node: Node): node is HTMLImageElement {
	return node.nodeName.toLowerCase() === 'img';
}

export function isTableElement(node: Node): node is HTMLTableElement {
	return node.nodeName.toLowerCase() === 'table';
}

export function isParagraphElement(node: Node): node is HTMLParagraphElement {
	return node.nodeName.toLowerCase() === 'p';
}

export function isBRElement(node: Node): node is HTMLBRElement {
	return node.nodeName.toLowerCase() === 'br';
}

export function isTextNode(node: Node): node is Text {
	return node.nodeName.toLowerCase() === '#text';
}

export async function convertToDataURL(blob: Blob): Promise<string> {
	return new Promise<string>((resolve, reject) => {
		const fileReader = new FileReader();
		fileReader.onload = () =>
			typeof fileReader.result === 'string' ? resolve(fileReader.result) : reject('invalid data');
		fileReader.onabort = () => reject('abort');
		fileReader.onerror = () => reject(fileReader.error?.message ?? 'error');
		fileReader.readAsDataURL(blob);
	});
}

export function imgToDataURL(img: HTMLImageElement): Promise<string> {
	return new Promise<string>((resolve) => {
		const image = new Image();
		image.crossOrigin = 'Anonymous';
		image.onload = () => {
			const canvas = document.createElement('canvas');
			const context = canvas.getContext('2d');
			if (!context) {
				resolve('');
				return;
			}
			canvas.height = image.naturalHeight;
			canvas.width = image.naturalWidth;
			context.drawImage(img, 0, 0);
			const dataURL = canvas.toDataURL('image/png');
			resolve(dataURL);
		};
		image.onabort = () => {
			resolve('');
		};
		image.onerror = () => {
			resolve('');
		};
		image.src = img.src;
	});
}

export async function loadImageToDataURL(url: string): Promise<string> {
	const result = await fetch(url);
	if (result.ok) {
		return await convertToDataURL(await result.blob());
	} else {
		throw new Error(`Load image failed: ${result.status}: ${result.statusText}`);
	}
}

export type TrimNodeContext = {
	pureTextFragment: DocumentFragment;
	destFragment: DocumentFragment;
	paragraph?: HTMLParagraphElement;
	pureText: boolean;
	images: Blob[];
	// images: string[];
};

export function getRtfImages(text: string, context: TrimNodeContext) {
	const reg = /{\\pict.*?(\\pngblip|\\jpegblip).*?{\\\*\\blipuid\s*[\da-fA-F]+}\s*([\da-fA-F]+)/g;
	let m: RegExpExecArray | null;
	while ((m = reg.exec(text))) {
		if (m[1] === '\\jpegblip') {
			const blob = hex2blob(m[2], 'image/jpeg');
			context.images.push(blob);
		} else if (m[1] === '\\pngblip') {
			const blob = hex2blob(m[2], 'image/png');
			context.images.push(blob);
		}
	}
}
// function getRtfImages(text: string, context: TrimNodeContext) {
// 	let regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/;
// 	let regexPicture = new RegExp(`(?:(${regexPictureHeader.source}))([\\da-fA-F\\s]+)\\}`, 'g');
// 	let images = text.match(regexPicture);
// 	if (!images) {
// 		// for wps
// 		regexPictureHeader = /{\\pict[\s\S]+?(\\pngblip-?\d+)?(\\wmetafile8-?\d+)?{\\\*\\blipuid\s?[\da-fA-F]+[\s}]*?/;
// 		regexPicture = new RegExp(`(?:(${regexPictureHeader.source}))([\\da-fA-F\\s]+)\\}`, 'g');
// 		images = text.match(regexPicture);
// 	}
// 	if (images) {
// 		for (const image of images) {
// 			let imageType;
// 			if (image.includes('\\pngblip')) {
// 				imageType = 'image/png';
// 			} else if (image.includes('\\jpegblip')) {
// 				imageType = 'image/jpeg';
// 			}
// 			if (imageType) {
// 				const hex = image.replace(regexPictureHeader, '').replace(/[^\da-fA-F]/g, '');
// 				// const base64 = `data:${imageType};base64,${hex2b64Ex(hex)}`;
// 				// context.images.push(base64);
// 				const blob = hex2blob(hex, imageType);
// 				context.images.push(blob);
// 			}
// 		}
// 	}
// }

export function askInsertTable(callback: (rows: number, cols: number) => void) {
	let rows: HTMLInputElement;
	let cols: HTMLInputElement;
	const dlg = Dialog.create(
		<div
			style={{
				textAlign: 'left',
				display: 'grid',
				gridTemplateColumns: getLanguage().startsWith('zh') ? '1fr 2fr' : '1fr',
				width: '16em',
				alignItems: 'center',
				rowGap: '0.5em',
			}}
		>
			<label>{DICT.rowCount}</label>
			<Input
				type="number"
				value="3"
				min="1"
				max="100"
				ref={(i) => (rows = i)}
				style={{ width: 'auto' }}
			/>
			<label>{DICT.columnCount}</label>
			<Input
				type="number"
				value="4"
				min="1"
				max="10"
				ref={(i) => (cols = i)}
				style={{ width: 'auto' }}
			/>
		</div>,
	)
		.setTitle(DICT.insertTable)
		.setEscClose(true)
		.setAutoClose(true)
		.setButtons(
			<ButtonPrimary
				onClick={() => {
					if (!/^\d+$/.test(rows.value)) {
						Dialog.error(DICT.invalidRowCount);
						return;
					}
					const rowCount = parseInt(rows.value);
					if (rowCount < 1 || rowCount > 500) {
						Dialog.error(`${DICT.invalidRowCount} ${DICT.shouldBeBetween} 1 ~ 500`);
						return;
					}

					if (!/^\d+$/.test(cols.value)) {
						Dialog.error(DICT.invalidColumnCount);
						return;
					}
					const colCount = parseInt(cols.value);
					if (colCount < 1 || colCount > 10) {
						Dialog.error(`${DICT.invalidColumnCount} ${DICT.shouldBeBetween} 1 ~ 10`);
						return;
					}
					dlg.close();
					callback(rowCount, colCount);
				}}
			>
				{DICT.ok}
			</ButtonPrimary>,
		)
		.show();
}

export function askInsertImage(callback: (src: string) => void) {
	selectFiles({ mimeType: 'image/*', multiple: false }).then((value) => {
		if (value.length > 0) {
			readFilesAsDataURL(value).then((dataURLs) => {
				callback(dataURLs[0]);
			});
		}
	});
}
